package csel.model;

import org.apache.log4j.Logger;

public abstract class MobileObject extends GameObject implements Ticked, Observable<Object>
{
	private static final long serialVersionUID = 6L;

	private static final Logger logger = Logger.getLogger("csel.model");

	/* observers */
	private final ObsHelper<Object> observers;

	private volatile Tile tile;
	private GameGlobals gameGlobals;
	private volatile boolean moving;
	private final String moveLock = new String("lolock");
	
	/* transient movement states */
	private int offX, offY;			// only acccessed from Chrono thread
	private volatile int tunm;		// ticks until next move
	
	public GameGlobals getGameGlobals()
	{
		return gameGlobals;
	}

	public MobileObject(String typeId)
	{
		super(typeId);
		
		observers = new ObsHelper<Object>(this);
		
		tile = null;
		gameGlobals = null;
		moving = false;
		offX = 0;
		offY = 0;
	}
	
	public void attach(GameGlobals gg)  // package private
	{
		gameGlobals = gg;
		
		logger.info("MobileObject attached to game.");
		
		gameGlobals.getChronoTrigger().addTicked(this);
	}
	
	public Tile getTile()
	{
		return tile;
	}
	
	public Location getLocation()
	{
		// TODO make this more efficient
		
		Location loc = tile.getLocation();
		return new Location(loc.getX() + offX, loc.getY() + offY);
	}
	
	public void tick()
	{
		if (tunm > 0)
		{
			tunm--;
		}
		else if (moving) /* (tunm <= 0)... can move */
		{
			moveOne();
			tunm = getSpeed();
		}
	}
	
	public void move(Direction direction)
	{
		synchronized (moveLock)
		{
			this.setDirection(direction);
			moving = true;
			moveOne(direction);
		}
	}
	
	/**
	 * Moves one tile in the specified direction. This will take speed into account.
	 * @param direction
	 * @return true if the movement was successful, false if it failed for any reason
	 */
	private boolean moveOne(Direction direction)
	{
		if (tunm > 0)	// NEED TO OVERRIDE THIS
		{
			logger.debug(String.format("%s can't move yet (%d ticks left).", this, tunm));
			return false;
		}
		
		return move(getTile().getNeighbor(direction));      // getTile() is important in case subclasses override!
	}
	
	private boolean moveOne()
	{
		synchronized (moveLock)
		{
			// getTile() is important in case subclasses override!
			return move(getTile().getNeighbor(getDirection()));
		}
	}
	
	/**
	 * Move immediately and directly to the specified tile. This will not take speed
	 * into account.
	 * @param dest the destination tile, or null to just leave the map
	 */
	public boolean move(Tile dest)
	{
		Tile src;
		
		synchronized (moveLock)
		{
			logger.debug(String.format("%s attempting to move to %s.", this, dest));
			
			src = tile;
			
			/* If this is our "first move" we're essentially just doing a setTile(). */
			if (src == null)
			{
				tile = dest;
				enterTile(dest);
				return true;
			}
			
			if (!src.canLeave(this))
			{
				logger.debug(String.format("%s could not move (stuck).", this));
				return false;
			}
			
			if (dest != null && !dest.canEnter(this, getBlockNotifier()))
			{
				logger.debug(String.format("%s could not move to %s (blocked).", this, dest));
				return false;
			}
			
			/* order is VERY FRIGGIN' IMPORTANT here */
			
			/* 1) Leave the last tile so that no weird state ends up happening. (It's
			 * better to have the Entity nowhere than to have it on two tiles.)
			 */
			leaveTile();
			
			/* 2) Set the new tile... this _must_ happen before entering the next tile
			 * to allow teleport tiles, etc. to work.
			 */
			tile = dest;
			 
			if (dest != null)
			{
				/* 3) Call enter on the new tile. This will now perform all the move effects,
				 * including teleport.
				 */
				enterTile(dest);
			}
			
			tunm = getSpeed();
			
			return true;
		}
	}
	
	public void stop()
	{
		moving = false;
	}
	
	/**
	 * A notification that the object has moved to a new tile. The object
	 * <em>must</em> call the appropriate enter() method on Tile.
	 */
	protected abstract void enterTile(Tile tile);

	/**
	 * A notification that the object has left its current tile. The object
	 * <em>must</em> call the appropriate leave() method on Tile.
	 */
	protected abstract void leaveTile();
	
	/**
	 * This is a callback that MobileObject will use when performing
	 * movement calculations. It will allow implementing objects to
	 * dynamically change their speeds.
	 * @return the current speed of this object
	 */
	protected abstract int getSpeed();
	
	/**
	 * This is a callback that any subclass can override if they want
	 * a block notifier. This notifier will automatically be used if non-null.
	 */
	protected BlockNotifier getBlockNotifier()
	{
		return null;
	}
	
	/* ABILITIES */
	
	public abstract boolean isImpassable();
	
	/**
	 * Note to overriders: just return super.can() if you don't override a certain ability!
	 * @return whether the MobileObject has the specified ability
	 */
	public boolean can(MoveAbilities ability)
	{
		return false;	// by default, no abilities
	}
	
	/* OBSERVER */
	
	public void addObserver(Observer<Object> o)
	{
		observers.add(o);
	}

	public boolean removeObserver(Observer<Object> o)
	{
		return observers.remove(o);
	}
	
	/**
	 * Just a convenience method for subclasses.
	 */
	protected void notifyO()
	{
		observers.notifyO(null);
	}
}
